import ryu
from ryu.base import app_manager
from ryu.controller import ofp_event, handler
from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER
from ryu.ofproto import ofproto_v1_3
from ryu.lib.packet import packet, ethernet, ether_types, ipv4, tcp
import os
import sys
from tqdm import tqdm

class SimpleSwitch13(app_manager.RyuApp):
    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]

    def __init__(self, *args, **kwargs):
        super(SimpleSwitch13, self).__init__(*args, **kwargs)
        self.mac_to_port = {}
        print("SimpleSwitch13 initialized")

    @handler.set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
    def switch_features_handler(self, ev):
        """
        Handler for switch features event.
        """
        try:
            self.install_table_miss_flow(ev.msg.datapath)
        except Exception as e:
            print(f"Exception in switch_features_handler: {e}")

    def install_table_miss_flow(self, datapath):
        """
        Install table-miss flow entry.
        """
        try:
            self.add_flow(datapath, 0, self.create_match(datapath), self.create_actions(datapath))
        except Exception as e:
            print(f"Exception in install_table_miss_flow: {e}")

    def create_match(self, datapath):
        """
        Create a match for the flow entry.
        """
        try:
            return datapath.ofproto_parser.OFPMatch()
        except Exception as e:
            print(f"Exception in create_match: {e}")

    def create_actions(self, datapath):
        """
        Create actions for the flow entry.
        """
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        return [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER, ofproto.OFPCML_NO_BUFFER)]

    def add_flow(self, datapath, priority, match, actions, buffer_id=None, idle_timeout=5):
        """
        Add a flow entry to the datapath.
        """
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS, actions)]
        mod = parser.OFPFlowMod(datapath=datapath, priority=priority, match=match, instructions=inst, buffer_id=buffer_id, idle_timeout=idle_timeout)
        datapath.send_msg(mod)

    @handler.set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
    def _packet_in_handler(self, ev):
        """
        Handler for packet in event.
        """
        try:
            self.handle_packet_truncation(ev)

            msg = ev.msg
            datapath = msg.datapath
            ofproto = datapath.ofproto
            parser = datapath.ofproto_parser
            in_port = msg.match['in_port']

            pkt = packet.Packet(msg.data)
            eth = pkt.get_protocols(ethernet.ethernet)[0]

            if eth.ethertype == ether_types.ETH_TYPE_LLDP:
                return

            self.handle_tcp_syn(msg, datapath, in_port, eth, pkt)

            dst, src, dpid = self.extract_packet_info(eth, datapath)
            self.update_mac_to_port(dpid, src, in_port)

            out_port = self.mac_to_port[dpid].get(dst, ofproto.OFPP_FLOOD)
            actions = [parser.OFPActionOutput(out_port)]

            if out_port != ofproto.OFPP_FLOOD:
                match = parser.OFPMatch(in_port=in_port, eth_dst=dst, eth_src=src)
                self.add_flow(datapath, 1, match, actions, msg.buffer_id if msg.buffer_id != ofproto.OFP_NO_BUFFER else None)

            data = msg.data if msg.buffer_id == ofproto.OFP_NO_BUFFER else None
            self.send_packet_out(datapath, msg, in_port, actions, data)
        except Exception as e:
            print(f"Exception in _packet_in_handler: {e}")

    def handle_packet_truncation(self, ev):
        """
        Handle packet truncation.
        """
        if ev.msg.msg_len < ev.msg.total_len:
            print("packet truncated: only %s of %s bytes", ev.msg.msg_len, ev.msg.total_len)

    def extract_packet_info(self, eth, datapath):
        """
        Extract packet information.
        """
        dst = eth.dst
        src = eth.src
        dpid = datapath.id
        print(f"packet in {dpid} {src} {dst} {eth.src}")
        return dst, src, dpid

    def update_mac_to_port(self, dpid, src, in_port):
        """
        Update MAC to port mapping.
        """
        self.mac_to_port.setdefault(dpid, {})
        self.mac_to_port[dpid][src] = in_port

    def send_packet_out(self, datapath, msg, in_port, actions, data):
        """
        Send packet out.
        """
        parser = datapath.ofproto_parser
        out = parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id, in_port=in_port, actions=actions, data=data)
        datapath.send_msg(out)

    def handle_tcp_syn(self, msg, datapath, in_port, eth, pkt):
        """
        Handle TCP SYN packets.
        """
        parser = datapath.ofproto_parser
        ofproto = datapath.ofproto

        if eth.ethertype == ether_types.ETH_TYPE_IP:
            ip_pkt = pkt.get_protocol(ipv4.ipv4)
            if ip_pkt and ip_pkt.proto == 6:  # IPPROTO_TCP
                tcp_pkt = pkt.get_protocol(tcp.tcp)
                if tcp_pkt and tcp_pkt.bits & tcp.TCP_SYN:
                    dst, src, dpid = self.extract_packet_info(eth, datapath)
                    self.update_mac_to_port(dpid, src, in_port)

                    out_port = self.mac_to_port[dpid].get(dst, ofproto.OFPP_FLOOD)
                    actions = [parser.OFPActionOutput(out_port)]

                    if out_port != ofproto.OFPP_FLOOD:
                        match = parser.OFPMatch(in_port=in_port, eth_dst=dst, eth_src=src, eth_type=ether_types.ETH_TYPE_IP, ip_proto=6, tcp_flags=tcp.TCP_SYN)
                        self.add_flow(datapath, 1, match, actions, msg.buffer_id if msg.buffer_id != ofproto.OFP_NO_BUFFER else None)

                    data = msg.data if msg.buffer_id == ofproto.OFP_NO_BUFFER else None
                    self.send_packet_out(datapath, msg, in_port, actions, data)

    def print_system_info(self):
        """
        Print system information.
        """
        print(f"System: {os.name}")
        print(f"Platform: {sys.platform}")
        print(f"Python version: {sys.version}")

    def simulate_progress(self):
        """
        Simulate progress using tqdm.
        """
        for i in tqdm(range(100), desc="Simulating progress"):
            pass

if __name__ == "__main__":
    app = SimpleSwitch13()
    app.print_system_info()
    app.simulate_progress()
